/*******************************************************************************
* 
* ResourceManager takes responsibility to read and write all the resources
* 
* 
******************************************************************************/
#include "resource-manager.h"
#include "process.h"
#include "strings.h"
#include "file-helper.h"
#include "lzma-helper.h"
#include "logging.h"
#include "json/json.h"

#include <windows.h>
#include <stack>
#include <algorithm>

using namespace std;
using namespace noria::diagnostic;
using namespace noria::text;

#define RES_META 1
#define RES_DATA 2

namespace noria{ namespace io
{
    inline bool ListFiles(const wstring& root, const wstring& mask, vector<wstring>& files);
    inline string getCate(const wstring& root, const wstring& file);
    inline string getName(const wstring& root, const wstring& file);

    ResourceManager::ResourceManager(pointer hMod)
    {
        LOG_DEBUG("ResourceManager::ctor()");
        init(hMod);
    }


    ResourceManager::~ResourceManager(void)
    {
        LOG_DEBUG("ResourceManager::~ctor()");
    }

    void ResourceManager::init(pointer pMod)
    {
        HMODULE hMod = (HMODULE)pMod;

        // find META in resource
        HRSRC hResInfo = FindResource(hMod, MAKEINTRESOURCE(RES_META), RT_RCDATA);
	    HGLOBAL hRes = LoadResource(hMod, hResInfo);
	    LPVOID ibuff = LockResource(hRes);
	    DWORD ilen = SizeofResource(hMod, hResInfo);
        LOG_DEBUG("Meta located, offset=%d, len=%d", ibuff, ilen);

        // extract meta
        size_t olen = ilen * 5;
        byte_ptr obuff = new byte[olen];
        bool ret = LzmaHelper::decompress((cbyte_ptr)ibuff, (size_t)ilen, obuff, &olen);
        if(!ret)
        {
            delete[] obuff;
            LOG_FATAL("Failure initializing ResourceManager: Failed to extract meta.");
            return;
        }

        // 
        string doc((char*)obuff, olen);
        delete[] obuff;

        // load meta
        Json::Reader reader;
        m_meta.reset(new Json::Value());
        ret = reader.parse(doc, *m_meta.get());
        if(!ret)
        {
            LOG_FATAL("Failure initializing ResourceManager: Failed to load meta.");
            return;
        }

        // find DATA in resource
        hResInfo = FindResource(hMod, MAKEINTRESOURCE(RES_DATA), RT_RCDATA);
	    hRes = LoadResource(hMod, hResInfo);
	    ibuff = LockResource(hRes);
	    ilen = SizeofResource(hMod, hResInfo);
        LOG_DEBUG("Data located, offset=%d, len=%d", ibuff, ilen);

        // load data
        m_data.reset(new binary((byte_ptr)ibuff, (byte_ptr)ibuff + ilen - 1));

    }

    binary_sp ResourceManager::get(const string& category, const string& name) const
    {
        if(!m_meta || !m_data)
        {
            LOG_FATAL("ResourceManager hasn't been initialized properly.");
            return binary_sp();
        }

        auto& root = *m_meta.get();
        auto& value = root[category][name]; // check if & is needed, what if there is no record match?

        // if there is no match
        if(value.empty())
        {
            LOG_ERROR("Resource not found, cat=%s, name=%s", category.c_str(), name.c_str());
            return binary_sp();
        }

        // get zipped size and original size
        int index = 0;
        size_t offset = value[index++].asUInt();
        size_t ilen = value[index++].asUInt();
        size_t olen = value[index++].asUInt();

        // get data pointer
        byte* data = &(*m_data.get())[offset];

        // create buffer
        binary_sp buffer(new binary(olen));

        // extract the data
        int hr = LzmaHelper::decompress(data, ilen, &(*buffer.get())[0], &olen);
        if(!hr)
        {
            LOG_ERROR("Failure extracting resource, cat=%s, name=%s", category.c_str(), name.c_str());
            return binary_sp();
        }

        return buffer;
    }


    void ResourceManager::package(const wstring& source, const wstring& dest) const
    {
        LOG_DEBUG(L"Traverse the source directory %s", source.c_str());
        Json::Value root;
        size_t total, ilen, olen;

        auto data_path = dest + L"/data.lzma";
        auto opath = dest + L"/manifest.js";
        auto opath2 = dest + L"/manifest.lzma";

        // remove old data files
        _wremove(data_path.c_str());
        _wremove(opath.c_str());
        _wremove(opath2.c_str());


        total = 0;
        vector<wstring> files;
        if (ListFiles(source, L"*", files)) 
        {
            LOG_DEBUG("%d files totally.", files.size());

            // bypass ./res/res.7z file
            files.erase(
                remove_if(files.begin(), files.end(),
                    [](const wstring& x) { 
                        bool ret = x.find(L"res\\res.7z") != string::npos; 
                        LOG_DEBUG(L"%s is going to be %s", x.c_str(), ret ? L"removed" : L"retained");
                        return ret;
            }), files.end());
                       
            LOG_DEBUG("%d files remained after skipping blacklist.", files.size());

            for (auto it = files.cbegin(); it != files.cend(); ++it) 
            {
                LOG_DEBUG(L"zipping %s...", it->c_str());

                // zip
                ilen = FileHelper::getFileLength(*it);
                olen = 0;
                int hr = LzmaHelper::compress(*it, data_path, &olen, true);
                if(!hr)
                {
                    LOG_ERROR("compress failed. terminating...");
                    return;
                }

                // write
                auto cat = getCate(source, *it);
                auto name = getName(source, *it);
                int index = 0;
                root[cat][name][index++] = (long)total;
                root[cat][name][index++] = (long)olen;
                root[cat][name][index++] = (long)ilen;
                LOG_DEBUG(L" total=%d, olen=%d, ilen=%d", total, olen, ilen);

                if(olen >= ilen)
                {
                    LOG_ERROR("wierd olen >= ilen. terminating...");
                    return;
                }

                // it++
                total += olen;
            }
        }


        ofstream of(opath, ios::binary);
        if(of.bad())
        {
            LOG_FATAL(L"Failure opening output file: %s", opath.c_str());
            return;
        }

        of << root.toStyledString();
        of.close();

        LzmaHelper::compress(opath, opath2);

        LOG_DEBUG(L"SUCCESSFULLY packaged.");
    }

    inline string getCate(const wstring& root, const wstring& file)
    {
        auto buff = file.substr(root.size() + 1);
        auto delimiter = buff.find_first_of(L'\\');
        auto value = buff.substr(0, delimiter);

        return Strings::w2a(value);
    }

    inline string getName(const wstring& root, const wstring& file)
    {
        auto buff = file.substr(root.size() + 1);
        auto delimiter = buff.find_first_of(L'\\');
        auto value = buff.substr(delimiter + 1);

        return Strings::w2a(value);
    }

    inline bool ListFiles(const wstring& root, const wstring& mask, vector<wstring>& files) 
    {
        HANDLE hFind = INVALID_HANDLE_VALUE;
        WIN32_FIND_DATA ffd;
        wstring spec;
        stack<wstring> directories;
        wstring path = root;

        directories.push(path);
        files.clear();

        while (!directories.empty()) {
            path = directories.top();
            spec = path + L"\\" + mask;
            directories.pop();

            hFind = FindFirstFile(spec.c_str(), &ffd);
            if (hFind == INVALID_HANDLE_VALUE)  {
                return false;
            } 

            do {
                if (wcscmp(ffd.cFileName, L".") != 0 && 
                    wcscmp(ffd.cFileName, L"..") != 0) {
                        if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
                            directories.push(path + L"\\" + ffd.cFileName);
                        }
                        else {
                            files.push_back(path + L"\\" + ffd.cFileName);
                        }
                }
            } while (FindNextFile(hFind, &ffd) != 0);

            if (GetLastError() != ERROR_NO_MORE_FILES) {
                FindClose(hFind);
                return false;
            }

            FindClose(hFind);
            hFind = INVALID_HANDLE_VALUE;
        }

        return true;
    }

}}